package csc.main;

import csc.transformation.CoordinatesTransformer;
import csc.transformation.InterpolationMethod;
import csc.approximation.LcaApproximationEquation;
import csc.approximation.AcaApproximationEquation;
import csc.approximation.Approximation;
import csc.segmentation.CentroidsCalculator;
import csc.segmentation.CentroidsCalculatorImpl;
import csc.transformation.Interpolator;
import csc.triangulation.Triangle;
import csc.triangulation.Triangulate;
import csc.triangulation.Vector3d;
import csc.util.Utils;
import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.gui.GenericDialog;
import ij.plugin.filter.PlugInFilter;
import ij.process.ImageProcessor;
import java.awt.Button;
import java.awt.Panel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Stanislau Dubrouski
 */
public class CSC implements PlugInFilter {

    private ImageStack stack;
    static final int X = 0;
    static final int Y = 1;
    static final int Z = 2;

    /*
     * Params for wrong values elimination. 
     * Values below are taken from 
     * 'An efficient algorithm for measurement 
     * and correction of chromatic aberrations 
     * in fluorescence microscopy.'
     */
    static float FIRST_PASS = 3.0f;
    static float SECOND_PASS = 4.0f;

    /**
     * Is called automatically before running the plugin.
     *
     * @param arg
     * @param imp
     * @return number representing plugin capabilities
     */
    public int setup(String arg, ImagePlus imp) {
        if (imp != null) {
            this.stack = imp.getStack();
        }
        return DOES_RGB + STACK_REQUIRED;
    }

    /**
     * Entry point to the plugin. Shows initial dialog with buttons for
     * different operations.
     *
     * @param ip ImageProcessor representing curently opened image. Parameter
     * not used here, processed image is set in setup method.
     */
    public void run(ImageProcessor ip) {
        GenericDialog initDialog = new GenericDialog("Choose operation:");
        initDialog.setModal(false);
        Panel p = new Panel();
        p.add(createAnalysisButton("Beads analysis", initDialog));
        p.add(createTransformationButton("Image transformation", initDialog));
        p.add(createMeanShiftsButton("Image statistics", initDialog));
        initDialog.addPanel(p);

        initDialog.showDialog();
    }

    
    private void analyzeImageAndComputeEquations() {
        AnalysisParameters usersParams = showDialogWithParams();
        if (usersParams == null) {
            return;
        }

        IJ.log("*** ");
        IJ.log("CSC image analysis execution began on " + new Date().toString() + "...");
        GenericDialog computingInProgress = createProgressBeganMessage();

        /*
         * Allow users set their own param values.
         */
        FIRST_PASS = usersParams.getFpass();
        SECOND_PASS = usersParams.getSpass();

        List<Colors> colors = getProcessedColors(usersParams);
        IJ.log("Following colors were chosen for processing - " + colors.toString());

        //start time counter
        long veryBeginning = System.currentTimeMillis();

        /*
         3D array like [x][y][z]
         processed image is converted to array to prevent
         image from being damaged by thresholding.
         3D array showed to be much faster than using ImageStack instance.
         */
        int[][][] image = CSC.convertTo3DArray(stack);

        Thresholder t = new BasicThresholderImpl();
        image = t.thresholdImage(image, usersParams.getThresholdLevel());
        IJ.log("Image has been thresholded with thresholding level set to " + usersParams.getThresholdLevel());

        CentroidsCalculator centroidsCalc = new CentroidsCalculatorImpl();
        List<List<Vector3d>> spots = centroidsCalc.computeCentroids(colors, image);

        spots = associateSpots(spots, usersParams.getMaxCentroidDistance());
        IJ.log("Centroids has been calculated. Max centroids distance was set to " + usersParams.getMaxCentroidDistance());

        //compute shift for every colors pair
        List<List<Shift>> ShiftsForColorPairs = createShiftVectorsForColorPairs(spots, colors);

        ShiftsForColorPairs = removeWrongShiftValues(ShiftsForColorPairs, FIRST_PASS);
        IJ.log("Shift filtering (stage 1) has been applied with coefficient set to " + usersParams.getFpass());

        ShiftsForColorPairs = removeWrongShiftValues(ShiftsForColorPairs, SECOND_PASS);
        IJ.log("Shift filtering (stage 2) has been applied with coefficient set to " + usersParams.getSpass());

        List<List<LcaApproximationEquation>> lcaApproxEquations
                = Approximation.calculateLcaApproximationEquations(ShiftsForColorPairs, colors);
        List<AcaApproximationEquation> acaApproxEquations
                = Approximation.calculateAcaApproximationEquations(ShiftsForColorPairs, colors);

        //save aprox. functions
        boolean funcsStoredSuccess = storeApproximationFunctions(lcaApproxEquations, acaApproxEquations, usersParams.getSaveFuncsTo());
        if (funcsStoredSuccess) {
            IJ.log("Approximation function have been computed and stored to " + usersParams.getSaveFuncsTo());
        } else {
            IJ.log("ERROR occured while writing computed functions to \"" + usersParams.getSaveFuncsTo() + "\"");
            computingInProgress.dispose();
            showText("An error occured while writing computed functions to file. Check logs for details.");
        }

        //create mock shifts to visualize shifts maps
        List<List<Shift>> calcShifts = modelShiftsBasedOnPlanesEquations(ShiftsForColorPairs, lcaApproxEquations, acaApproxEquations);

        //print mocks to form shifts visualization
        Visualizer vis = new Visualizer(image.length, image[0].length, ShiftsForColorPairs, calcShifts, colors);
        boolean reportStoredSuccess = vis.generateReport(usersParams.getReportPath(), "Measured and approximated shifts visualization");

        if (reportStoredSuccess) {
            IJ.log("Results report's been generated and saved to " + usersParams.getReportPath());
        } else {
            IJ.log("ERROR occured while writing generated report to \"" + usersParams.getReportPath() + "\"");
            computingInProgress.dispose();
            showText("An error occured while writing generated report. Check logs for details.");
        }

        long endTime = System.currentTimeMillis();
        long elapsedTime = endTime - veryBeginning;
        IJ.log("CSC image analysis execution ended on " + new Date().toString() + ", execution took " + elapsedTime + " ms.");
        IJ.log("*** ");
        computingInProgress.dispose();
        showResultsDialog(elapsedTime);
    }

    private void executeTransformation() {
        TransformParameters params = showDialogGetTransformParams();
        if (params == null) {
            return;
        }

        IJ.log("*** ");
        IJ.log("CSC transformation execution began on " + new Date().toString());
        GenericDialog computingInProgress = createProgressBeganMessage();

        //start time counter
        long veryBeginning = System.currentTimeMillis();

        IJ.log("Selected reference color " + params.getReferenceColor());
        IJ.log("Selected interpolation method " + params.getInterpolationMethod().toString());

        CoordinatesTransformer transformer;
        try {
            transformer = readApproximationFunctions(params.getFunctionsFilePath());
        } catch (ApproxFunctionsReadException ex) {
            computingInProgress.dispose();
            showText("An error occured while reading approximation functions. Check logs for details.");
            return;
        }

        if (!checkColors(params.getReferenceColor(), transformer)) {
            computingInProgress.dispose();
            return;
        }
        //[x][y][z]
        int[][][] image = CSC.convertTo3DArray(stack);
        image = transformImage(image, params.getInterpolationMethod(),
                params.getReferenceColor(), transformer);

        int[][] toStack = convertTo2DArray(image);
        updateStack(toStack);

        long endTime = System.currentTimeMillis();
        long elapsedTime = endTime - veryBeginning;
        IJ.log("CSC: transformation completed. Transformation took " + elapsedTime + " ms.");
        IJ.log("*** ");
        computingInProgress.dispose();
        showResultsDialog(elapsedTime);
    }

    private void computeMeanShifts() {
        //start time counter
        long veryBeginning = System.currentTimeMillis();
        IJ.log("*** ");
        IJ.log("CSC: image statistics calculation began on " + new Date().toString() + "...");

        GenericDialog calcBegan = createProgressBeganMessage();

        IJ.showStatus("Thresholding image...");

        List<Colors> colors = new ArrayList<Colors>();
        colors.add(Colors.RED);
        colors.add(Colors.GREEN);
        colors.add(Colors.BLUE);

        int[][][] image = CSC.convertTo3DArray(stack);
        Thresholder t = new BasicThresholderImpl();
        image = t.thresholdImage(image, 120);

        IJ.showStatus("Calculating centroids...");

        CentroidsCalculator centroidsCalc = new CentroidsCalculatorImpl();
        List<List<Vector3d>> spots = centroidsCalc.computeCentroids(colors, image);
        IJ.showStatus("Associating spots...");

        spots = associateSpots(spots, 15);
        IJ.showStatus("Converting shifts...");

        //compute shift for every colors pair
        List<List<Shift>> ShiftsForColorPairs = createShiftVectorsForColorPairs(spots, colors);
        IJ.showStatus("Filtering shifts stage 1...");

        ShiftsForColorPairs = removeWrongShiftValues(ShiftsForColorPairs, FIRST_PASS);
        IJ.showStatus("Filtering shifts stage 2...");

        ShiftsForColorPairs = removeWrongShiftValues(ShiftsForColorPairs, SECOND_PASS);
        IJ.showStatus("Creating report...");

        List<Float> means = new ArrayList<Float>();
        List<Float> devs = new ArrayList<Float>();

        int colorpair = -1;
        for (List<Shift> s : ShiftsForColorPairs) {
            colorpair++;
            IJ.log("Mean shift value for color pair " + colorpair + " is " + Shift.calculateMeanShift(s) + ";");
            means.add(Shift.calculateMeanShift(s));
            IJ.log("Standard deviation for color pair " + colorpair + " is " + Shift.calculateShiftStandardDeviation(s) + ";");
            devs.add(Shift.calculateShiftStandardDeviation(s));
        }
        IJ.showStatus("Computing mean values...");

        IJ.log("-- Overall mean shift for image is " + Utils.computeMeanValue(means));
        IJ.log("-- Overall std. deviation for image is " + Utils.computeMeanValue(devs));
        IJ.showStatus("Done.");

        long endTime = System.currentTimeMillis();
        long elapsedTime = endTime - veryBeginning;
        IJ.log("CSC: image statistics calculation ended on " + new Date().toString());
        calcBegan.dispose();
        showResultsDialog(elapsedTime);
    }

    
    private Button createAnalysisButton(String label, final GenericDialog dialog) {
        Button analysisButton = new Button(label);
        analysisButton.setLocation(150, 150);
        analysisButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                dialog.dispose();
                analyzeImageAndComputeEquations();
            }
        });
        return analysisButton;
    }

    private Button createTransformationButton(String label, final GenericDialog dialog) {
        Button transformButton = new Button(label);
        transformButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                dialog.dispose();
                executeTransformation();
            }
        });
        return transformButton;
    }

    private Button createMeanShiftsButton(String label, final GenericDialog dialog) {
        Button meanShiftButton = new Button(label);
        meanShiftButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                dialog.dispose();
                computeMeanShifts();
            }
        });
        return meanShiftButton;
    }

    
    private boolean checkColors(Colors color, CoordinatesTransformer transformer) {
        if (!transformer.includesColor(color)) {
            showText("Color chosen as reference color hasn't been found in approximation equations file. "
                    + "Try to choose another color or re-analyze beads with other colors selected.");
            return false;
        } else {
            return true;
        }
    }

    private CoordinatesTransformer readApproximationFunctions(String filePath) throws ApproxFunctionsReadException {
        List<List<LcaApproximationEquation>> lcaApproxEquationsRestored = null;
        List<AcaApproximationEquation> acaApproxEquationsRestored = null;

        File file = new File(filePath);
        try {
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            lcaApproxEquationsRestored = (List<List<LcaApproximationEquation>>) ois.readObject();
            acaApproxEquationsRestored = (List<AcaApproximationEquation>) ois.readObject();
        } catch (IOException ex) {
            IJ.log("ERROR: Approximation functions couldn't be read, filepath is \"" + filePath + "\"");
            throw new ApproxFunctionsReadException("Error while reading functions from " + filePath, ex);
        } catch (ClassNotFoundException ex) {
            System.err.println(ex);
            IJ.log("ERROR: Approximation functions couldn't be read, filepath is \"" + filePath + "\"");
            throw new ApproxFunctionsReadException("Error while reading functions from " + filePath, ex);
        }

        return new CoordinatesTransformer(lcaApproxEquationsRestored, acaApproxEquationsRestored);
    }

    private boolean storeApproximationFunctions(List<List<LcaApproximationEquation>> lcas, List<AcaApproximationEquation> acas, String filename) {
        //write found functions to file
        File file = new File(filename);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(lcas);
            oos.writeObject(acas);
            return true;
        } catch (FileNotFoundException ex) {
            System.err.println(ex);
            Logger.getLogger(CSC.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        } catch (IOException ex) {
            System.err.println(ex);
            Logger.getLogger(CSC.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
    }

    private int[][][] transformImage(int[][][] image,
            InterpolationMethod method, Colors refColor, CoordinatesTransformer cd) {
        System.err.println("Image transformation started.");

        int[][][] correctedImage = new int[stack.getWidth()][stack.getHeight()][stack.getSize()];
        for (int x = 0; x < image.length; x++) {
            for (int y = 0; y < image[x].length; y++) {
                for (int z = 0; z < image[x][y].length; z++) {
                    int red = (int) (image[x][y][z] & 0xff0000) >> 16;
                    int green = (int) (image[x][y][z] & 0x00ff00) >> 8;
                    int blue = (int) (image[x][y][z] & 0x0000ff);

                    //get colors that are to be transformed 
                    ArrayList<Colors> transformedColors = new ArrayList<Colors>();
                    transformedColors.add(Colors.RED);
                    transformedColors.add(Colors.GREEN);
                    transformedColors.add(Colors.BLUE);
                    transformedColors.remove(refColor);

                    Vector3d originalCoordinates = new Vector3d(x, y, z);
                    Vector3d transCoord1
                            = cd.transformCoordinates(originalCoordinates, refColor, transformedColors.get(0));
                    Vector3d transCoord2
                            = cd.transformCoordinates(originalCoordinates, refColor, transformedColors.get(1));

                    int interpolated1 = 0;
                    int interpolated2 = 0;

                    interpolated1 = Interpolator.interpolate(image, transCoord1, transformedColors.get(0), method);
                    interpolated2 = Interpolator.interpolate(image, transCoord2, transformedColors.get(1), method);

                    if (refColor == Colors.RED) {
                        correctedImage[x][y][z] = ((red & 0xff) << 16) + ((interpolated1 & 0xff) << 8) + (interpolated2 & 0xff);
                    }
                    if (refColor == Colors.GREEN) {
                        correctedImage[x][y][z] = ((interpolated1 & 0xff) << 16) + ((green & 0xff) << 8) + (interpolated2 & 0xff);
                    }
                    if (refColor == Colors.BLUE) {
                        correctedImage[x][y][z] = ((interpolated1 & 0xff) << 16) + ((interpolated2 & 0xff) << 8) + (blue & 0xff);
                    }

                }
            }
        }
        return correctedImage;
    }

    private List<Vector3d> getBeginVectorsListFromShifts(List<Shift> input) {
        List<Vector3d> result = new ArrayList<Vector3d>();
        for (Shift pair : input) {
            result.add(new Vector3d(pair.getBeginVector().getX(), pair.getBeginVector().getY(), pair.getBeginVector().getZ()));
        }
        return result;
    }

    private List<List<Shift>> removeWrongShiftValues(List<List<Shift>> input, final float coeff) {
        for (List<Shift> shifts : input) {
            List<Vector3d> spots = getBeginVectorsListFromShifts(shifts);
            List<Triangle> triangles = Triangulate.triangulate((ArrayList<Vector3d>) Utils.removeNullValues(spots));
            List<Shift> wrongVals = getWrongShiftValues(shifts, triangles, coeff);
            shifts.removeAll(wrongVals);
        }
        return input;
    }

    private List<Shift> getWrongShiftValues(
            List<Shift> pairs, List<Triangle> triangles, final float coeff) {

        List<Vector3d> averageNeighbourVectors = new ArrayList<Vector3d>();
        for (Shift pair : pairs) {
            averageNeighbourVectors.add(getAverageNeighbourVector(pair, pairs, triangles));
        }

        List<Float> diffs = new ArrayList<Float>();
        for (int i = 0; i < pairs.size(); i++) {
            Vector3d vectorValue = pairs.get(i).computeShiftValue();
            diffs.add(new Float(Vector3d.computeNormVector(
                    Vector3d.substractVectors(vectorValue, averageNeighbourVectors.get(i)))));
        }

        float meanDiff = Utils.computeMeanValue(diffs);
        float deviation = Utils.computeStandartDeviation(diffs);
        List<Shift> wrongValues = new ArrayList<Shift>();
        float diff;

        float measure = coeff * deviation;
        for (int i = 0; i < pairs.size(); i++) {
            diff = Math.abs(diffs.get(i) - meanDiff);
            if (diff > measure) {
                wrongValues.add(new Shift(pairs.get(i)));
            }
        }
        return wrongValues;
    }

    private Vector3d getAverageNeighbourVector(Shift pair, List<Shift> pairs, List<Triangle> triangles) {
        List<Shift> neighbours = getNeighbourPairsOfVectors(pair, pairs, triangles);
        return computeMeanVectorFromNeighbors(pair, neighbours);
    }

    /**
     *
     * @param pair
     * @param input
     * @return mean shifts calculated from given shift's neighbors
     */
    protected static Vector3d computeMeanVectorFromNeighbors(Shift pair, Collection<Shift> input) {
        Vector3d weightedVectorsSum = new Vector3d(0, 0, 0);
        float weightsSum = 0;

        for (Shift neighbour : input) {
            Vector3d v = neighbour.computeShiftValue();
            float distance = Vector3d.computeVectorsDistance(pair.getBeginVector(), neighbour.getBeginVector());
            float weight = 1 / distance;

            weightedVectorsSum.add(Vector3d.multiplyVector(v, weight));
            weightsSum += weight;
        }
        return Vector3d.divideVector(weightedVectorsSum, weightsSum);
    }

    private List<Shift> getNeighbourPairsOfVectors(Shift pair, List<Shift> pairsOfVectors, List<Triangle> triangles) {
        List<Shift> result = new ArrayList<Shift>();

        Collection<Vector3d> neighbourSpots = getNeighbourSpots(pair.getBeginVector(), triangles);

        for (Shift pv : pairsOfVectors) {
            if (neighbourSpots.contains(pv.getBeginVector())) {
                result.add(pv);
            }
        }

        return result;
    }

    private Collection<Vector3d> getNeighbourSpots(Vector3d pv, List<Triangle> triangles) {
        Set<Vector3d> result = new HashSet<Vector3d>();
        List<Triangle> trianglesWithSpot = getTrianglesWithSpot(pv, triangles);

        for (Triangle tr : trianglesWithSpot) {
            result.add(new Vector3d(tr.getP1().getX(), tr.getP1().getY(), tr.getP1().getZ()));
            result.add(new Vector3d(tr.getP2().getX(), tr.getP2().getY(), tr.getP2().getZ()));
            result.add(new Vector3d(tr.getP3().getX(), tr.getP3().getY(), tr.getP3().getZ()));
        }

        result.remove(pv);
        return result;
    }

    /**
     *
     * @param vector
     * @param triangles
     * @return triangles, containing given vector as on of the vertexes
     */
    protected static List<Triangle> getTrianglesWithSpot(Vector3d vector, List<Triangle> triangles) {
        List<Triangle> result = new ArrayList<Triangle>();

        for (Triangle tr : triangles) {
            if (triangleContainsVector(vector, tr)) {
                result.add(tr);
            }
        }
        return result;
    }

    /**
     *
     * @param pv
     * @param tr
     * @return true if ine of the vertexes in triangle tr equals to given
     * vector.
     */
    protected static boolean triangleContainsVector(Vector3d pv, Triangle tr) {
        return tr.getP1().equals(pv)
                || tr.getP2().equals(pv)
                || tr.getP3().equals(pv);
    }

    private List<Colors> getProcessedColors(AnalysisParameters usersParams) {
        List<Colors> result = new ArrayList<Colors>();
        if (usersParams.getProcessRed()) {
            result.add(Colors.RED);
        }
        if (usersParams.getProcessGreen()) {
            result.add(Colors.GREEN);
        }
        if (usersParams.getProcessBlue()) {
            result.add(Colors.BLUE);
        }

        return result;
    }

    private static int[][][] convertTo3DArray(ImageStack stack) {
        int xSize = stack.getWidth();
        int ySize = stack.getHeight();
        int zSize = stack.getSize();
        int[][][] resultImage = new int[stack.getWidth()][stack.getHeight()][stack.getSize()];

        for (int z = 0; z < zSize; z++) {
            int[] pixels = (int[]) stack.getPixels(z + 1);
            for (int x = 0; x < xSize; x++) {
                for (int y = 0; y < ySize; y++) {
                    resultImage[x][y][z] = pixels[y * xSize + x];
                }
            }
        }
        return resultImage;
    }

    private static int[][] convertTo2DArray(int[][][] image) {

        int[][] resultImage = new int[image[0][0].length][image.length * image[0].length];

        for (int x = 0; x < image.length; x++) {
            for (int y = 0; y < image[x].length; y++) {
                for (int z = 0; z < image[x][y].length; z++) {
                    resultImage[z][y * image.length + x] = image[x][y][z];
                }
            }
        }
        return resultImage;
    }

    private void updateStack(int[][] image) {
        System.out.println("Stack updating began");
        for (int z = 0; z < image.length; z++) {
            int[] pixels = (int[]) stack.getPixels(z + 1);
            for (int i = 0; i < image[0].length; i++) {
                pixels[i] = image[z][i];
            }
        }
    }

    private AnalysisParameters showDialogWithParams() {
        AnalysisParameters params = new AnalysisParameters();
        //fill params with default values
        params.setFpass(FIRST_PASS);
        params.setSpass(SECOND_PASS);
        params.setMaxCentroidDistance(10);
        params.setProcessRed(true);
        params.setProcessGreen(true);
        params.setProcessBlue(true);
        params.setReportPath(System.getProperty("user.dir") + File.separator + "plugins"
                + File.separator + "csc_analysis_report.html");
        params.setReportRequired(true);
        params.setSaveFuncsTo(System.getProperty("user.dir") + File.separator + "plugins"
                + File.separator + "csc_approx_functions.dat");
        params.setThresholdLevel(120);

        String errors = "";
        final Float nanConst = new Float(Float.NaN);

        while (true) {
            errors = "";

            final GenericDialog gd = new GenericDialog("Set parameters to proceed...");
            gd.addMessage("Colors, that are to be processed:");
            gd.addCheckbox("Red", params.getProcessRed());
            gd.addCheckbox("Green", params.getProcessGreen());
            gd.addCheckbox("Blue", params.getProcessBlue());
            gd.addNumericField("Thresholding level: ", params.getThresholdLevel(), 0);
            gd.addNumericField("Maximum distance between centroids (px): ", params.getMaxCentroidDistance(), 0);
            gd.addNumericField("Coefficient for 1st filtering cycle: ", params.getFpass(), 2);
            gd.addNumericField("Coefficient for 2nd filtering cycle: ", params.getSpass(), 2);
            gd.addStringField("Save approximation functions to:", params.getSaveFuncsTo(), 50);

            gd.addCheckbox("Generate report:", true);
            gd.addStringField("Save report to:", params.getReportPath(), 50);

            gd.showDialog();
            if (gd.wasCanceled()) {
                return null;
            }

            params.setProcessRed(gd.getNextBoolean());
            params.setProcessGreen(gd.getNextBoolean());
            params.setProcessBlue(gd.getNextBoolean());
            List<Colors> processedColors = getProcessedColors(params);
            if (processedColors.size() < 2) {
                errors = errors.concat("\nChoose at least two colors.");
            }
            params.setThresholdLevel((int) gd.getNextNumber());
            if (params.getThresholdLevel() < 1 || params.getThresholdLevel() > 255) {
                errors = errors.concat("\nThreshold level must be a number between 1 and 255.");
            }
            params.setMaxCentroidDistance((int) gd.getNextNumber());
            if (params.getMaxCentroidDistance() == 0) {
                errors = errors.concat("\nMaximum centroids distance is incorrect. Cannot be 0.");
            }
            params.setFpass((float) gd.getNextNumber());
            if (nanConst.equals(params.getFpass())) {
                errors = errors.concat("\nCoefficient for 1st cycle of filtering is incorrect.");
            }
            params.setSpass((float) gd.getNextNumber());
            if (nanConst.equals(params.getSpass())) {
                errors = errors.concat("\nCoefficient for 2nd cycle of filtering is incorrect.");
            }
            params.setSaveFuncsTo((String) gd.getNextString());
            if (params.getSaveFuncsTo().isEmpty()) {
                errors = errors.concat("\nApproximation functions file's path is empty.");
            }
            params.setReportRequired(gd.getNextBoolean());
            if (params.isReportRequired()) {
                params.setReportPath((String) gd.getNextString());
            } else {
                params.setReportPath(null);
            }
            if (params.isReportRequired()) {
                if (params.getReportPath().isEmpty()) {
                    errors = errors.concat("\nReport file's path is empty.");
                }
            }

            if (!errors.isEmpty()) {
                GenericDialog error = new GenericDialog("Error");
                error.addMessage("Some errors were found:");
                error.addMessage(errors);
                error.showDialog();
            } else {
                break;
            }
        }

        return params;
    }

    private TransformParameters showDialogGetTransformParams() {

        TransformParameters params = new TransformParameters();
        //set default values
        params.setFunctionsFilePath(System.getProperty("user.dir") + File.separator + "plugins"
                + File.separator + "csc_approx_functions.dat");
        
        String errors = "";
        
        while (true) {
            errors = "";
            GenericDialog paramsDialog = new GenericDialog("Set parameters to proceed...");

            paramsDialog.addStringField("Path to approximation functions file:",
                    params.getFunctionsFilePath(), 50);
            paramsDialog.addChoice("Color channel to be used as reference:",
                    new String[]{"Red", "Green", "Blue"}, "Red");
            paramsDialog.addChoice("Interpolation method:",
                    new String[]{"Nearest neighbor", "Trilinear", "Tricubic"}, "Trilinear");
            paramsDialog.showDialog();
            if (paramsDialog.wasCanceled()) {
                return null;
            }
            params.setFunctionsFilePath(paramsDialog.getNextString());
            if (params.getFunctionsFilePath().isEmpty()){
                errors = errors.concat("\nFile name cannot be empty.");
            }
            String refColor = paramsDialog.getNextChoice();
            params.setReferenceColor(Colors.valueOf(refColor.toUpperCase()));

            String interMethod = paramsDialog.getNextChoice();
            InterpolationMethod method = InterpolationMethod.TRILINEAR;
            if ("Nearest neighbor".equals(interMethod)) {
                method = InterpolationMethod.NEAREST_NEIGHBOR;
            }
            if ("Tricubic".equals(interMethod)) {
                method = InterpolationMethod.TRICUBIC;
            }
            params.setInterpolationMethod(method);
            
            if (!errors.isEmpty()) {
                GenericDialog error = new GenericDialog("Error");
                error.addMessage("Some errors were found:");
                error.addMessage(errors);
                error.showDialog();
            } else {
                break;
            }
        }

        return params;
    }

    private void showText(String text) {
        GenericDialog results = new GenericDialog("Here are some results");
        results.addMessage(text);
        results.showDialog();
    }

    private void showResultsDialog(long elapsedTime) {
        GenericDialog results = new GenericDialog("Here are some results");
        results.addMessage("Execution took " + elapsedTime + " ms.");
        results.showDialog();
    }

    private List<List<Vector3d>> associateSpots(List<List<Vector3d>> spots, int maxCentroidDistance) {

        if (spots.size() == 1) {
            return spots;
        }

        List<List<Vector3d>> result = new ArrayList<List<Vector3d>>();

        for (int color = 0; color < spots.size(); color++) {
            result.add(new ArrayList<Vector3d>());
        }

        while (true) {
            int spotTakenFrom = -1;

            for (int color = 0; color < spots.size(); color++) {
                if (!spots.get(color).isEmpty()) {
                    result.get(color).add(spots.get(color).get(0));
                    spots.get(color).remove(0);
                    spotTakenFrom = color;
                    break;
                }
            }

            if (spotTakenFrom < 0) {
                break;
            }

            //insert nulls for already emptied colors
            for (int color = 0; color < spotTakenFrom; color++) {
                result.get(color).add(null);
            }

            //find closest spots for every not-empty color
            for (int color = spotTakenFrom + 1; color < spots.size(); color++) {
                Vector3d closestSpot = findClosestSpot(result.get(spotTakenFrom).get(result.get(spotTakenFrom).size() - 1), spots.get(color), maxCentroidDistance);
                result.get(color).add(closestSpot);
                spots.get(color).remove(closestSpot);
            }
        }

        return result;
    }

    private Vector3d findClosestSpot(Vector3d spot, List<Vector3d> spots, int maxCentroidDistance) {
        Vector3d result = null;

        if (spots.isEmpty()) {
            return null;
        }

        float maxDistance = 1000;

        for (int i = 0; i < spots.size(); i++) {
            if ((Vector3d.computeVectorsDistance(spot, spots.get(i)) < maxCentroidDistance) && (Vector3d.computeVectorsDistance(spot, spots.get(i)) < maxDistance)) {
                maxDistance = Vector3d.computeVectorsDistance(spot, spots.get(i));
                result = spots.get(i);
            }
        }
        return result;
    }

    private List<List<Shift>> createShiftVectorsForColorPairs(List<List<Vector3d>> pVectorLists, List<Colors> colors) {
        List<List<Shift>> shiftVectors = new ArrayList<List<Shift>>();

        //create Shifts from 1. to 2. color in the colors list. Since there are always
        //at least two colors in the list, no check are needed.
        shiftVectors.add(new ArrayList<Shift>());
        for (int point = 0; point < pVectorLists.get(0).size(); point++) {
            if (pVectorLists.get(0).get(point) == null || pVectorLists.get(1).get(point) == null) {
                continue;
            }
            shiftVectors.get(0).add(new Shift(pVectorLists.get(0).get(point), pVectorLists.get(1).get(point)));
        }

        /*
         if all 3 colors are being processed, 
         create 2-3 and 3-1 color shifts
         */
        if (colors.size() == 3) {
            //2. color to 3. color shift
            shiftVectors.add(new ArrayList<Shift>());
            for (int point = 0; point < pVectorLists.get(1).size(); point++) {
                if (pVectorLists.get(1).get(point) == null || pVectorLists.get(2).get(point) == null) {
                    continue;
                }
                shiftVectors.get(1).add(new Shift(pVectorLists.get(1).get(point), pVectorLists.get(2).get(point)));
            }

            //3. color to 1. color shift
            shiftVectors.add(new ArrayList<Shift>());
            for (int point = 0; point < pVectorLists.get(2).size(); point++) {
                if (pVectorLists.get(2).get(point) == null || pVectorLists.get(0).get(point) == null) {
                    continue;
                }
                shiftVectors.get(2).add(new Shift(pVectorLists.get(2).get(point), pVectorLists.get(0).get(point)));
            }
        }

        return shiftVectors;
    }

    private List<List<Shift>> modelShiftsBasedOnPlanesEquations(List<List<Shift>> ShiftsForColorPairs,
            List<List<LcaApproximationEquation>> lca, List<AcaApproximationEquation> aca) {
        float FIELD_WIDTH = 1024f;
        float FIELD_HEIGHT = 1024f;
        float SPOTS_DIST_VERT = 75f;
        float SPOTS_DIST_HOR = 75f;

        /*
         * build mock shifts based on calculated planes to visualize approximated shifts map.
         */
        List<List<Shift>> calcShifts = new ArrayList<List<Shift>>();
        for (int colorPair = 0; colorPair < ShiftsForColorPairs.size(); colorPair++) {
            calcShifts.add(new ArrayList<Shift>());
            for (int i = 25; i < FIELD_WIDTH; i += SPOTS_DIST_VERT) {
                for (int j = 25; j < FIELD_HEIGHT; j += SPOTS_DIST_HOR) {
                    calcShifts.get(colorPair).add(
                            new Shift(
                                    new Vector3d(i, j, 0),
                                    new Vector3d(i + lca.get(colorPair).get(0).calculateEstimatedShiftValue(i, j),
                                            j + lca.get(colorPair).get(1).calculateEstimatedShiftValue(i, j),
                                            aca.get(colorPair).calculateEstimatedShiftValue(i, j))));

                }
            }
        }
        return calcShifts;
    }

    private GenericDialog createProgressBeganMessage() {
        GenericDialog gd = new GenericDialog("Calculation began");
        gd.setModal(false);
        gd.addMessage("Calculation in progress, wait please...");
        gd.showDialog();
        Button[] buttons = gd.getButtons();
        buttons[0].disable();
        buttons[1].disable();

        return gd;
    }

}
